﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents a beam.
    /// </summary>
    public class Beam : Attachment
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Beam"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal Beam(XmlPackagePart packagePart, Staff owner)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            beamedSymbolsCollection = new ElementReferenceCollection<DurationalSymbol>(this, beamedSymbolsElement);
            beamedSymbols = new ReadOnlyCollection<DurationalSymbol>(beamedSymbolsCollection);
        }

        /// <summary>
        /// Creates a new <see cref="Beam"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Beam(XmlPackagePart packagePart, Staff owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            y1Attribute = element.Attributes[PackageVocabulary.Y1Attribute];
            y2Attribute = element.Attributes[PackageVocabulary.Y2Attribute];
            beamedSymbolsElement = element[PackageVocabulary.BeamedSymbolsElementName, PackageVocabulary.PackageNamespaceUri];

            beamedSymbolsCollection = new ElementReferenceCollection<DurationalSymbol>(this, beamedSymbolsElement);
            beamedSymbols = new ReadOnlyCollection<DurationalSymbol>(beamedSymbolsCollection);
        }

        #endregion

        #region Attachment Members

        /// <inheritdoc/>
        protected override XmlElement CreateAttachmentElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BeamElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            beamedSymbolsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BeamedSymbolsElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            y1Attribute = document.CreateAttribute(PackageVocabulary.Y1Attribute);
            y2Attribute = document.CreateAttribute(PackageVocabulary.Y2Attribute);

            element.Attributes.Append(y1Attribute);
            element.Attributes.Append(y2Attribute);

            element.AppendChild(beamedSymbolsElement);

            return element;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the property of some symbol has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the modified property is <see cref="CoreSymbol.Position"/>
        /// the handler calls the <see cref="AdjustStemLengths"/> method.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void symbol_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Position")
                AdjustStemLengths();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Adjusts the <see cref="Chord.StemLength"/> of beamed chords so that they end in the beam.
        /// </summary>
        private void AdjustStemLengths()
        {
            var xDistance = beamedSymbolsCollection[beamedSymbolsCollection.Count - 1].Position - beamedSymbolsCollection[0].Position;
            if (xDistance == 0)
                return;
            var yDistance = Y2 - Y1;

            foreach (DurationalSymbol symbol in beamedSymbolsCollection)
            {
                if (symbol is Chord)
                {
                    var chord = (Chord)symbol;
                    var t = (symbol.Position - beamedSymbolsCollection[0].Position) / xDistance;
                    var stemEnd = Y1 + t * yDistance;

                    if (chord.StemDirection == StemOrientation.Up && chord.GetStemStart() < stemEnd)
                        chord.StemDirection = StemOrientation.Down;
                    else if (chord.StemDirection == StemOrientation.Down && chord.GetStemStart() > stemEnd)
                        chord.StemDirection = StemOrientation.Up;
                    
                    var realEnd = chord.GetStemEnd();
                    var diff = stemEnd - realEnd;
                    if (chord.StemDirection == StemOrientation.Up)
                        chord.StemLength -= diff;
                    else
                        chord.StemLength += diff;
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Groups given symbols under a beam.
        /// </summary>
        /// <remarks>
        /// <para>
        /// <b>Important:</b> The <paramref name="symbols"/> collection has to contain
        /// at least two elements and the first and the last one have to be <see cref="Chord"/>s.
        /// Also the symbols have to belong to the same voice.
        /// </para>
        /// <para>
        /// If the <paramref name="symbols"/> were already beamed the original beams will
        /// be joined.
        /// </para>
        /// <para>
        /// The <paramref name="symbols"/> are expected to be ordered ascendently by their beat.
        /// This not checked, but the result of passing a collection with a different ordering
        /// is not defined.
        /// </para>
        /// </remarks>
        /// <param name="symbols">
        /// <para>
        /// Type: <see cref="IList{T}"/> of <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The symbols to be grouped.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Beam"/>
        /// </para>
        /// <para>
        /// The beam under which the symbols were grouped.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="symbols"/> collection is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="symbols"/> collection contains less than 2 elements.<br/>
        /// -or-<br/>
        /// The first or the last symbol is not a <see cref="Chord"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The symbols belong to different voices.
        /// </exception>
        public static Beam BeamGroup(IList<DurationalSymbol> symbols)
        {
            if (symbols == null)
                throw new ArgumentNullException("symbols");
            if (symbols.Count < 2)
                throw new ArgumentException("The symbols collection must contain at least two elements!");
            if (!(symbols[0] is Chord) || !(symbols[symbols.Count - 1] is Chord))
                throw new ArgumentException("The first and the last symbol in the symbols collection must be Chords!");

            var voices = (from symbol in symbols
                          select symbol.PresentedElement.Voice).Distinct();

            if (voices.Count() > 1)
                throw new InvalidOperationException("The symbols must belong to the same voice!");

            var beams = (from symbol in symbols
                        where symbol.GroupBeam != null
                        select symbol.GroupBeam).Distinct();

            var beamsEnumerator = beams.GetEnumerator();
            Beam result;

            if (beamsEnumerator.MoveNext())
                result = beamsEnumerator.Current;
            else
            {
                var staff = symbols[0].FindOwner<Staff>();
                result = new Beam(staff.PackagePart, staff);
                staff.Attachments.Add(result);
            }

            while (beamsEnumerator.MoveNext())
            {
                foreach (DurationalSymbol symbol in beamsEnumerator.Current.BeamedSymbols)
                {
                    symbol.PropertyChanged -= beamsEnumerator.Current.symbol_PropertyChanged;
                    symbol.GroupBeam = result;
                }
                beamsEnumerator.Current.FindOwner<Staff>().Attachments.Remove(beamsEnumerator.Current);
            }

            foreach (DurationalSymbol symbol in result.beamedSymbolsCollection)
                symbol.PropertyChanged -= result.symbol_PropertyChanged;
            result.beamedSymbolsCollection.Clear();

            result.Y1 = ((Chord)symbols[0]).GetStemEnd();
            result.Y2 = ((Chord)symbols[symbols.Count - 1]).GetStemEnd();

            foreach (DurationalSymbol symbol in symbols)
            {
                result.beamedSymbolsCollection.Add(symbol);
                symbol.GroupBeam = result;
                symbol.PropertyChanged += new PropertyChangedEventHandler(result.symbol_PropertyChanged);
            }

            result.AdjustStemLengths();

            return result;
        }

        /// <summary>
        /// Restores the beamed symbols collection.
        /// </summary>
        /// <param name="symbols">
        /// <para>
        /// Type: <see cref="IList{T}"/> of <see cref="Notoric.Model.Elements.CWNotation.DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The restored beamed symbols collection.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="symbols"/> collection is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="symbols"/> collection contains less than 2 elements.<br/>
        /// -or-<br/>
        /// The first or the last symbol is not a <see cref="Chord"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The symbols belong to different voices.
        /// </exception>
        public void RestoreBeam(IList<DurationalSymbol> symbols)
        {
            if (symbols == null)
                throw new ArgumentNullException("symbols");
            if (symbols.Count < 2)
                throw new ArgumentException("The symbols collection must contain at least two elements!");
            if (!(symbols[0] is Chord) || !(symbols[symbols.Count - 1] is Chord))
                throw new ArgumentException("The first and the last symbol in the symbols collection must be Chords!");

            var voices = (from symbol in symbols
                          select symbol.PresentedElement.Voice).Distinct();

            if (voices.Count() > 1)
                throw new InvalidOperationException("The symbols must belong to the same voice!");

            for (int i = 0; i < symbols.Count; ++i)
            {
                if (!beamedSymbolsCollection.Contains(symbols[i]))
                {
                    symbols[i].PropertyChanged += new PropertyChangedEventHandler(symbol_PropertyChanged);
                    beamedSymbolsCollection.Insert(i, symbols[i]);
                    symbols[i].GroupBeam = this;
                }
            }

            AdjustStemLengths();
        }

        /// <summary>
        /// Splits previously beamed symbols.
        /// </summary>
        /// <remarks>
        /// <para>
        /// If the <paramref name="symbols"/> are part of a larger beamed group
        /// the original beam will be split into two parts (before and after the <paramref name="symbols"/>).
        /// </para>
        /// <para>
        /// The <paramref name="symbols"/> have to all be a part of the same beam.
        /// </para>
        /// </remarks>
        /// <param name="symbols">
        /// <para>
        /// Type: <see cref="IList{T}"/> of <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The symbols to be split.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="symbols"/> collection is <see langword="null"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The <paramref name="symbols"/> are not a part of the same beam.<br/>
        /// -or-<br/>
        /// The <paramref name="symbols"/> do not form a continuous subgroup in the beam.
        /// </exception>
        public static void SplitGroup(IList<DurationalSymbol> symbols)
        {
            if (symbols == null)
                throw new ArgumentNullException("symbols");
            if (symbols.Count == 0)
                return;

            var beams = (from symbol in symbols
                         select symbol.GroupBeam).Distinct();
            if (beams.Count() > 1)
                throw new InvalidOperationException("The symbols are not a part of the same beam.");
            if (beams.Count() == 0)
                return;

            var beam = beams.First();
            int beamStart = beam.BeamedSymbols.IndexOf(symbols[0]);
            int beamStop = beamStart;

            for (int i = 0; i < symbols.Count; ++i, ++beamStop)
            {
                if (symbols[i] != beam.BeamedSymbols[beamStop])
                    throw new InvalidOperationException("The symbols do not form a continuous subgroup in the beam.");
            }

            if (beamStart > 0 && beamStop < beam.BeamedSymbols.Count)
            {
                var toNewBeam = new List<DurationalSymbol>(beam.beamedSymbolsCollection.Count - beamStop);

                for (int i = beam.BeamedSymbols.Count - 1; i >= beamStop ; --i)
                {
                    beam.BeamedSymbols[i].GroupBeam = null;
                    toNewBeam.Add(beam.beamedSymbolsCollection[i]);
                    beam.beamedSymbolsCollection[i].PropertyChanged -= beam.symbol_PropertyChanged;
                    beam.beamedSymbolsCollection.RemoveAt(i);
                }

                BeamGroup(toNewBeam);
            }

            for (int i = beamStop - 1; i >= beamStart; --i)
            {
                beam.beamedSymbolsCollection[i].GroupBeam = null;
                beam.beamedSymbolsCollection[i].PropertyChanged -= beam.symbol_PropertyChanged;
                beam.beamedSymbolsCollection.RemoveAt(i);
            }

            if (beam.beamedSymbolsCollection.Count == 0)
                beam.FindOwner<Staff>().Attachments.Remove(beam);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of the symbols grouped by this beam.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="IEnumerable{T}"/> of <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The collection of symbols that are grouped by this beam.
        /// </para>
        /// </value>
        public ReadOnlyCollection<DurationalSymbol> BeamedSymbols
        {
            get { return beamedSymbols; }
        }

        /// <summary>
        /// Gets or sets the y coordinate of the beam's start point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The vertical offset (in centimeters) of the beam's start point
        /// from the bottom of the owner <see cref="Staff"/>.
        /// </para>
        /// </value>
        public double Y1
        {
            get { return double.Parse(y1Attribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                y1Attribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("Y1");
            }
        }

        /// <summary>
        /// Gets or sets the y coordinate of the beam's end point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The vertical offset (in centimeters) of the beam's end point
        /// from the bottom of the owner <see cref="Staff"/>.
        /// </para>
        /// </value>
        public double Y2
        {
            get { return double.Parse(y2Attribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                y2Attribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("Y2");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="BeamedSymbols"/> property.
        /// </summary>
        private ReadOnlyCollection<DurationalSymbol> beamedSymbols;
        /// <summary>
        /// Holds the collection of beamed symbols.
        /// </summary>
        private ElementReferenceCollection<DurationalSymbol> beamedSymbolsCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="beamedSymbols"/> collection.
        /// </summary>
        private XmlElement beamedSymbolsElement;
        /// <summary>
        /// Holds the value of the <see cref="Y1"/> property.
        /// </summary>
        private XmlAttribute y1Attribute;
        /// <summary>
        /// Holds the value of the <see cref="Y2"/> property.
        /// </summary>
        private XmlAttribute y2Attribute;

        #endregion
    }
}
